package gridy.algorithm;

import gridy.fitness.FitnessCalculator;
import gridy.geneticoperations.Mutation;
import gridy.geneticoperations.PolynomialMutation;
import gridy.geneticoperations.Recombination;
import gridy.geneticoperations.SBXRecombination;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;

import org.apache.log4j.Logger;
import org.osoa.sca.annotations.EagerInit;
import org.osoa.sca.annotations.Init;
import org.osoa.sca.annotations.Reference;
import org.osoa.sca.annotations.Scope;

@EagerInit
@Scope("COMPOSITE")
public class GeneticAlgorithmImpl implements GeneticAlgorithm {
    
    static Logger log = Logger.getLogger(GeneticAlgorithmImpl.class);
    
    private int stepsNum;
    private int genesNum = 10;
    private int individualsNum = 200;
    
    private double recombinationProb = 0.8;
  
    private Mutation mutation;
    private Recombination recombination;
    
    @Reference
    protected FitnessCalculator fitnessCalculator;
    
    public GeneticAlgorithmImpl() {
        
        log.info("Tworze GeneticAlgorithm");
        mutation = new PolynomialMutation(5.0, 0.1);
        recombination = new SBXRecombination(5.0);
    }
    
    @Init
    public void init() {
        setStepNum(400);
        start();
    }
    
    @Override
    public void start() {
        
        log.info("GeneticAlgorithm rozpoczął działanie");
        long startTime = System.currentTimeMillis();
        
        IndividualImpl best = null;
        List<IndividualImpl> population = new ArrayList<IndividualImpl>(individualsNum); 
        for (int indNum = 0; indNum < individualsNum; indNum++) {
            population.add(createRandomIndividual());
        }
        assert population.size() == individualsNum;
        calculateValues(population);
        
        for (int stepNum = 0; stepNum < this.stepsNum; stepNum++) {
            
            List<IndividualImpl> matingPool = createMatingPool(population);
            assert matingPool.size() == individualsNum;
            population = createNextPopulation(matingPool);
            assert population.size() == individualsNum;
            
            calculateValues(population);
            
            if (stepNum % 10 == 0) {
                
                IndividualImpl nowBest = Collections.max(population);
                if (best == null || nowBest.compareTo(best) == 1) {
                    best = nowBest;
                }
                log.info("step = "+stepNum+" nowBest = "+nowBest.getFitness()+" best = "+best.getFitness());
                
            }
        }
        
        long period = System.currentTimeMillis() - startTime;
        log.info(String.format("GeneticAlgorithm zakonczyl dzialanie, %d krokow w czasie %d.%d sekund",
                stepsNum, period / 1000, period % 1000));
    }

    @Override
    public void setStepNum(int stepNum) {
        this.stepsNum = stepNum;
    }
    
    /**
     * Zleca obliczenie wartosci fitness dla wszystkich osobników z populacji.
     * @param population
     */
    private void calculateValues(List<IndividualImpl> population) {
        
        double[] problem = new double[population.size()*genesNum];
        for (int i = 0; i < population.size(); i++) {
            IndividualImpl ind = population.get(i);
            for (int j = 0; j < genesNum; j++) {
                problem[i*genesNum+j] = ind.getGene(j);
            }
        }
        
        String msg = String.format(
                "Wysylam zadanie obliczenia, problem.length = %d, numOfProblems = %d",
                problem.length, population.size());
        log.debug(msg);
        
        double[] result = fitnessCalculator.calculate(problem, population.size());
        for (int i = 0; i < population.size(); i++) {
            population.get(i).setValue(result[i]);
        }
    }

    /**
     * Roulette Wheel selection
     * @param population
     * @return
     */
    private List<IndividualImpl> createMatingPool(
            List<IndividualImpl> population) {
        
        assert population.size() > 0;

        double fitnessSum = 0.0;
        for (IndividualImpl ind : population) {
            assert ind.getFitness() > 0 : ind.getFitness();
            fitnessSum += ind.getFitness();
        }

        Random rand = new Random();
        List<IndividualImpl> matingPool = new ArrayList<IndividualImpl>(population.size());
        for (int i = 0; i < population.size(); i++) {
            double r = rand.nextDouble() * fitnessSum;
            double rangeStart = 0.0;
            for (IndividualImpl ind : population) {
                double rangeEnd = rangeStart + ind.getFitness();
                if (r > rangeStart && r < rangeEnd)
                    matingPool.add((IndividualImpl) ind.duplicate());
                rangeStart = rangeEnd;
            }
        }
        return matingPool;
        
    }

    private IndividualImpl createRandomIndividual() {
        
        Random rand = new Random();
        
        IndividualImpl result = new IndividualImpl(this.genesNum);
        double range = result.getBoundry(1) - result.getBoundry(0);
        
        for (int i = 0; i < this.genesNum; i++) {
            
            double value = result.getBoundry(0) + (range * rand.nextDouble());
            result.setGene(i, value);
            
        }
        return result;
    }

    private List<IndividualImpl> createNextPopulation(
            List<IndividualImpl> population) {
        
        Random rand = new Random();
        List<IndividualImpl> newPopulation = new ArrayList<IndividualImpl>(population.size());
        int populationSize = population.size();

        if ((population.size() % 2) != 0) {
            newPopulation.add(population.remove(0));
        }
        for (int i = 0; i < (populationSize / 2); i++) {
            IndividualImpl ind1 = population.remove(rand.nextInt(population.size()));
            IndividualImpl ind2 = population.remove(rand.nextInt(population.size()));

            IndividualImpl child1 = null;
            IndividualImpl child2 = null;
            if (Math.random() < this.recombinationProb) {
                
                List<Individual> childs = recombination.recombine(ind1, ind2);
                child1 = (IndividualImpl)childs.get(0);
                child2 = (IndividualImpl)childs.get(1);
            } else {
                mutation.mutate(ind1);
                mutation.mutate(ind2);
                child1 = ind1;
                child2 = ind2;
            }

            assert child1 != null && child2 != null;
            newPopulation.add(child1);
            newPopulation.add(child2);
        }

        assert newPopulation.size() == populationSize : "newPop" + newPopulation.size() + "," + populationSize;
        return newPopulation;
    }

}
